package com.eddie.auction.web;

import com.eddie.auction.utils.PrintUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serial;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义Web框架的核心控制器类
 *
 * @author Eddie
 * @date 2022/08/01 16:13
 **/
public class DispatcherServlet extends HttpServlet {
    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 默认的配置文件名,可以在web.xml中通过初始化参数configFileName修改
     */
    private String configFileName = "myweb.xml";

    /**
     * 配置信息初始化器，加载配置文件并保存
     */
    private ControllerMappingManager ctrMappingMgr;

    /**
     * 存放Controller实力的HashMap
     * key为Controller的全类名，value为key所指的类的实例
     */
    protected final Map<String, Object> controllers = new HashMap<>();

    /**
     * 为每个Controller实例存放其方法实例，key为方法的映射信息,value为Method实例
     */
    protected final Map<String, Method> methods = new HashMap<>();

    public void init() {
        //读取web.xml中提供的配置文件的位置信息
        String configFileName = this.getServletConfig().getInitParameter("configFileName");
        if (!(configFileName == null || (configFileName = configFileName.trim()).equals("")))
            this.configFileName = configFileName;
        //初始化加载映射配置文件中的信息
        ctrMappingMgr = new ControllerMappingManager(this.configFileName);
        synchronized (controllers) {
            controllers.clear();
        }
        synchronized (methods) {
            methods.clear();
        }
    }

    public void destroy() {
        super.destroy();
        synchronized (controllers) {
            controllers.clear();
        }
        synchronized (methods) {
            methods.clear();
        }
        this.ctrMappingMgr.getControllerMappings().clear();
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        execute(req, resp);
    }

    /**
     * 根据映射信息获取Controller的调用信息并调用
     * 映射信息形式为：/aaa/.../zzz
     */
    protected void execute(HttpServletRequest request, HttpServletResponse response) {
        String path;
        //先考虑按前缀映射取出Servlet访问路径中包含的映射信息
        //即Servlet采用/xxx/*的映射方式
        //取到的映射信息是/xxx后面的部分，包含/开头
        path = (String) request.getAttribute("javax.servlet.include.path_info");
        //取不到说明不是包含动作，则正常取值
        if (path == null) {
            path = request.getPathInfo();
            //依然为空说明不是按前缀映射
            if (path == null || (path = path.trim()).isEmpty()) {
                //在考虑按扩展名映射取出Servlet访问路径中包含的映射信息，
                //即Servlet采用*.xxx的映射方式，取到的映射信息也包含扩展名部分
                //先看一下是不是包含动作<jsp:include>发出的请求，如果是则此项有值
                path = (String) request.getAttribute("javax.servlet.include.servlet_path");
                //娶不到说明不是包含动作，则正常取值
                if (path == null) {
                    path = request.getServletPath();
                }
                //把扩展名截掉
                int slash = path.lastIndexOf("/");
                int period = path.lastIndexOf(".");
                if ((period >= 0) && (period > slash)) {
                    path = path.substring(0, period);
                }
            }
        }
        //如果根据path找不到Controller的信息，抛出PathException异常,这里没写就先用运行时异常代替
        if (!ctrMappingMgr.containsKey(path)) {
            throw new RuntimeException("No Controller & Method is mapped with this path :" + path);
        }
        ControllerMapping controllerMapping = ctrMappingMgr.getControllerMapping(path);
        try {
            Object instance = null;
            //根据得到的全类名，创建该类的唯一实例
            synchronized (controllers) {
                //从集合属性controllers中查找已创建的Controller实例
                instance = controllers.get(controllerMapping.getClassName());
                //如未找到，则通过反射加载，并放入集合
                if (instance == null) {
                    instance = Class.forName(controllerMapping.getClassName()).getDeclaredConstructor().newInstance();
                    controllers.put(controllerMapping.getClassName(), instance);
                }
            }
            //调用指定方法
            Method method = null;
            boolean hasServlet = false;
            synchronized (methods) {
                //从集合属性methods中查找已创建的Method实例
                method = methods.get(path);
                //如未找到，则通过反射加载，并放入集合
                if (method == null) {
                    Class<?> aClass = instance.getClass();
                    try {
                        method = aClass.getMethod(controllerMapping.getMethodName(), HttpServletRequest.class, HttpServletResponse.class);
                        hasServlet=true;
                        //如果报错表示没找到，那就试试找一下不带Request和resp的方法
                    } catch (NoSuchMethodException e) {
                        method = aClass.getMethod(controllerMapping.getMethodName(),new Class[0]);
                    }
                    methods.put(path, method);
                }
            }
            Object result = hasServlet?method.invoke(instance, request, response):method.invoke(instance);
            //对响应模式进行统一处理
            toView(result, request, response);
        } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException |
                 NoSuchMethodException | ServletException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected void toView(Object result, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        if (request == null) return;
        //字符串类型表示String
        if (result instanceof String url) {
            //是否为重定向
            boolean isRedirect = false;
            //如果以redirect开始，则设置为重定向
            if (url.startsWith("redirect:")) {
                isRedirect = true;
                //设置url为redirect：后面的路径
                url = url.substring("redirect:".length());
            } else if (url.startsWith("forward:")) { //判断是否为转发
                //设置url为转发后面的路径
                url = url.substring("forward:".length());
            }

            //导航路径必须以"/"开始，表示上下文根路径
            if (!(url = url.trim()).startsWith("/")) throw new RuntimeException();
            if (isRedirect)
                response.sendRedirect(request.getContextPath() + url);
            else
                request.getRequestDispatcher(url)
                        .forward(request, response);
        } else { //其他Java对象类型以JSON格式的响应返回
            PrintUtils.write(result, response);
        }
    }
}
